Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/三件套/JS/JavaScript

08 原型、构造函数与类

33 分钟阅读 · Note

目录树 578 篇

            • 00 JavaScript学习地图
            • 01 JavaScript基础
            • 02 类型系统与类型转换
            • 03 Number与String
            • 04 Array数组
            • 05 Object对象
            • 06 集合与迭代
            • 07 函数、作用域与this
            • 08 原型、构造函数与类
            • 09 Generator与异步迭代
            • 10 异步编程
            • 11 浏览器与DOM
            • 12 二进制与文件
            • 13 标准内建对象
            • 14 模块化
            • 15 错误处理
            • 16 JSDoc
            • 90 JavaScript知识点总目录
            • 91 JavaScript修正与补充记录
          • 前端模块化
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗00 JavaScript学习地图反向链接↗90 JavaScript知识点总目录反向链接↗01 JavaScript基础同一路径↗02 类型系统与类型转换同一路径↗03 Number与String同一路径↗04 Array数组同一路径
  • 08 原型、构造函数与类

08 原型、构造函数与类

> Last Format Time:8/11/2026 21:45:39

本笔记由两个 JavaScript 教程目录中的同主题内容合并而成;全部旧知识保留在“旧笔记知识全集(按来源保留)”中,并由导航逐项索引。


当前前端开发关键要点

> [!important] 学习优先级:P1 > - 理解实例、构造函数、prototype 与原型链的关系,能读懂第三方库和面试中的继承代码。 > - 业务代码优先使用 class 表达对象模型,但要知道它仍建立在原型机制之上。 > - 组合通常比深层继承更易维护;Mixin、私有字段和静态成员应按清晰边界使用。 > - 掌握 new 的执行过程以及 instanceof 的原型链判断逻辑。


知识点导航

[现代JS教程与阮一峰ES6 · 2函数/构造函数与New](./08 原型、构造函数与类.md)

  • 构造函数
  • 手动实现一个new

[现代JS教程与阮一峰ES6 · 4原型与类/原型](./08 原型、构造函数与类.md)

  • 构造函数的Prototype
  • 原生的原型
  • 原型方法和没有__proto__的对象

[现代JS教程与阮一峰ES6 · 4原型与类/原型链](./08 原型、构造函数与类.md)

  • 原型链

[现代JS教程与阮一峰ES6 · 4原型与类/类](./08 原型、构造函数与类.md)

  • 基本语法
  • 类表达式
  • Getters/setters
  • 计算属性名称
  • “类字段”
  • 类继承
  • 重写方法
  • 静态属性和静态方法
  • 受保护的、私有的属性和方法
  • 私有的属性
  • 扩展内建类(使用内建类做父类)
  • 类检查
  • Mixin模式
  • 🤔 为什么需要 Mixin 模式?
  • 🛠️ 如何实现 Mixin 模式?
  • 使用 Object.assign() 进行属性复制
  • 使用高阶函数(类工厂)
  • ✨ Mixin 模式的优缺点
  • 优点
  • 缺点
  • 🌐 实际应用:Vue.js 中的 Mixin

[廖雪峰教程 · 6对象/6.0 简述](./08 原型、构造函数与类.md)

  • 正文主题:6.0 简述

[廖雪峰教程 · 6对象/6.1 创建对象](./08 原型、构造函数与类.md)

  • 创建对象
  • 构造函数
  • 忘记写new怎么办

[廖雪峰教程 · 6对象/6.2 原型继承](./08 原型、构造函数与类.md)

  • 原型继承

[廖雪峰教程 · 6对象/6.3 类的继承](./08 原型、构造函数与类.md)

  • 类的继承
  • class继承

现代前端补充与纠错

> [!info] 修改标记 > - 【修正】:旧教程中错误、过时或容易误导的内容。 > - 【补充】:旧教程未覆盖、但当前前端开发需要掌握的内容。 > - 【修正代码】:替换或校正了旧代码示例。

> [!warning] 下方保留教程原文;class 是基于原型机制的语法体系,但不能把所有 class 行为机械等同于旧式构造函数写法。

原型与 class

  • 【补充】 new 会创建对象、连接原型、以该对象调用构造器,并按构造器返回值规则决定最终结果。
  • 【补充】 class 声明不会像函数声明那样完整提升,主体默认严格模式,方法不可直接作为构造器调用。
  • 【补充】 公有字段是实例自身属性;#private 私有字段由语言强制约束,不等同于命名约定 _private。
  • 【补充】 静态字段和静态块适合类级初始化,但要注意模块求值时的副作用和测试隔离。

设计建议

  • 【补充】 继承适合稳定的“是一个”关系;前端组件和业务模型通常优先组合、委托与小型纯函数。
  • 【补充】 修改内建原型会影响全局环境并可能与未来标准冲突,除受控 polyfill 外应避免。
  • 【修正】 跨 realm 判断不要盲信 instanceof;数组优先 Array.isArray,其他对象结合领域 API 判断。

旧笔记知识全集(按来源保留)

现代JS教程与阮一峰ES6 · 2函数/构造函数与New

构造函数

构造函数在技术上是常规函数。不过有两个约定:

  1. 它们的命名以==大写字母==开头。
  2. 它们==只能由 "new"== 操作符来执行。

当一个函数被使用 new 操作符执行时,它按照以下步骤:

  1. 一个==新的空对象==被创建并==绑定给 this==。(如果是内置的对象还要链接到原型链)
  2. 函数体执行。通常它会修改 this,为其添加新的属性。
  3. 返回 this 的值。构造函数没有 return 语句。

但是,如果这有一个 return 语句,那么规则就简单了:

  • 如果 return 返回的是一个对象,则返回这个对象,而不是 this。
  • 如果 return 返回的是一个原始类型,则忽略。

从技术上讲,任何函数(除了箭头函数,它没有自己的 this)都可以用作构造器。即可以通过 new 来运行,它会执行上面的算法。“首字母大写”是一个共同的约定,以明确表示一个函数将被使用 new 来运行。

function User(name, age = 0) {
    this.name = name;
    this.age = age;
    this.run = function () {
        console.log('run');
    }
    this.walk = () => {
        console.log('walk');
    }
}

let vipUser = new User('dano', 21);

console.log(vipUser.name);
vipUser.run();
vipUser.walk();
手动实现一个new

在面试题手撸集合中

现代JS教程与阮一峰ES6 · 4原型与类/类

基本语法

在面向对象的编程中,class是用于创建对象的可扩展的程序代码模版,它为对象提供了状态(成员变量)的初始值和行为(成员函数或方法)的实现。

基本语法:

class MyClass {
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
  ...
}

然后使用 new MyClass() 来创建具有上述列出的所有方法的新对象。new 会自动调用 constructor() 方法,因此我们可以在 constructor() 中初始化对象。

class是一个函数,就是constructor构造函数。

例如:

class User {
  constructor(name) {
	this.name = name;
  }

  sayHi() {
	  alert(this.name);
  }
}

// class 是一个函数
alert(typeof User); // function

// ...或者,更确切地说,==是 constructor 方法==
alert(User === User.prototype.constructor); // true

// 方法在 User.prototype 中,例如:
alert(User.prototype.sayHi); // sayHi 方法的代码

// 在原型中实际上有两个方法
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

![Pasted image 20250626231602](../../../../_assets/7aac320-Pasted image 20250626231602.png)

人们常说 class 是一个语法糖(旨在使内容更易阅读,但不引入任何新内容的语法),因为我们实际上可以在不使用 class 的情况下声明相同的内容:

// 用纯函数重写 class User

// 1. 创建构造器函数
function User(name) {
  this.name = name;
}
// 函数的原型(prototype)默认具有 "constructor" 属性,
// 所以,我们不需要创建它

// 2. 将方法添加到原型
User.prototype.sayHi = function() {
  alert(this.name);
};

// 用法:
let user = new User("John");
user.sayHi();

这个定义的结果与使用类得到的结果基本相同。因此,这确实是将 class 视为一种定义构造器及其原型方法的语法糖的理由。

尽管,它们之间存在着重大差异:

  1. 首先,通过 class 创建的函数==具有特殊的内部属性标记== IsClassConstructor: true。因此,它与手动创建并不完全相同。
  2. 类方法不可枚举。类定义将 "prototype" 中的所有方法的 enumerable 标志设置为 false。这很好,因为如果我们对一个对象调用 for..in 方法,我们通常不希望 class 方法出现。
  3. 类总是使用 use strict。在类构造中的所有代码都将自动进入严格模式。

就我的个人理解来说,就是在语法糖的基础上加入了一些新的特性,基本上就差不多

类表达式

就和函数表式一样:

let User = class {
  sayHi() {
    alert("Hello");
  }
};

类似于命名函数表达式(Named Function Expressions),类表达式可能也应该有一个名字。 如果类表达式有名字,那么该名字仅在类内部可见:

// “命名类表达式(Named Class Expression)”
// (规范中没有这样的术语,但是它和命名函数表达式类似)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // MyClass 这个名字仅在类内部可见
  }
};

new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容

alert(MyClass); // error,MyClass 在外部不可见
Getters/setters

就像对象字面量,类可能包括 getters/setters,计算属性(computed properties)等。 这是一个使用 get/set 实现 user.name 的示例:

class User {

  constructor(name) {
    // 调用 setter
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short.");
      return;
    }
    this._name = value;
  }

}

let user = new User("John");
alert(user.name); // John

user = new User(""); // Name is too short.

从技术上来讲,这样的类声明可以通过在 User.prototype 中创建 getters 和 setters 来实现。

对象中也有类似的访问器属性,类中的getter、setter本质上都是访问器属性(Accessor Property) ,表现形式完全相同。

类 getter/setter 对象访问器属性 位置 在 User.prototype 上 在对象自身上 作用范围 所有实例共享 仅当前对象 ** enumerable** 默认 false 默认 true

从运行结果可以清楚看到:

类属性描述符:
{ get: [Function], set: [Function], enumerable:
false, configurable: true }

对象属性描述符:
{ get: [Function], set: [Function], enumerable:
true, configurable: true }

两者都有 get 和 set ,这证明 它们是完全一样的数据结构 。

图示

类的 getter/setter:
user ──────proto─────> User.prototype
                              ↓
                        { get, set }  ← 所有实例共用

对象的访问器属性:
obj ──直接拥有──> { _name, get, set }  ← 只属于这个对
象

简单记忆:类的 getter/setter 是写在原型上的"实例共享版"访问器属性。

计算属性名称
class User {

  ['say' + 'Hi']() {
    alert("Hello");
  }

}

new User().sayHi();
“类字段”

是一种允许添加任何属性的语法。 例如,让我们在 class User 中添加一个 name 属性:

class User {
  name = "John";

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi(); // Hello, John!

类字段的重要区别在于,它们会被挂在实例对象上,而非 User.prototype 上 ![Pasted image 20260412193153](../../../../_assets/35ff722-Pasted image 20260412193153.png)

正如 函数绑定 一章中所讲的,JavaScript 中的函数具有动态的 this。它取决于调用上下文。 因此,如果一个对象方法被传递到某处,或者在另一个上下文中被调用,则 this 将不再是对其对象的引用。 类字段提供了一种很优雅的写法:

class Button {
  constructor(value) {
    this.value = value;
  }
  click = () => {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // hello
class Button {
    constructor(value) {
        this.value = value;
    }

    click() {
        console.log(this.value);
    }
}

let button = new Button("hello");

setTimeout(button.click, 1000); // undefined
setTimeout(() => button.click(), 1000);//hello
类继承

继承另一个类的语法是:class Child extends Parent ![Pasted image 20250626234203](../../../../_assets/fa8d5ae-Pasted image 20250626234203.png) 在内部,关键字 extends 使用了很好的旧的原型机制进行工作。它==将 Rabbit.prototype.Prototype(隐式原型) 设置为 Animal.prototype==。所以,如果在 Rabbit.prototype 中找不到一个方法,JavaScript 就会从 Animal.prototype 中获取该方法。

类语法不仅允许指定一个类,在 extends 后可以指定任意表达式。

重写方法

"super" 关键字。

  • 执行 super.method(...) 来调用一个父类方法。
  • 执行 super(...) 来调用一个父类 constructor(只能在我们的 constructor 中)。
class Animal {
    constructor(name) {
        this.name = name;
    }

    run() {
        console.log('animal run');
    }

}

class Rabbit extends Animal {
    run() {
        super.run();//animal run
        console.log('rabbit run');//rabbit run
    }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run();

箭头函数没有 super。如果被访问,它会从外部函数获取。

如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的“空” constructor:

class Rabbit extends Animal {
  // 为没有自己的 constructor 的扩展类生成的
  constructor(...args) {
    super(...args);
  }
}

继承类的 constructor 必须调用 super(...),并且 一定要在使用 this 之前调用。

在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 ConstructorKind:"derived"。这是一个特殊的内部标签。

该标签会影响它的 new 行为:

  • 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this。
  • 但是当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作。

因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建。并且我们会收到一个报错。

为了让 Rabbit 的 constructor 可以工作,它需要在使用 this 之前调用 super(),就像下面这样:重写constructor

class Animal {
    constructor(name) {
        this.name = name;
    }

    run() {
        console.log('animal run');
    }

}

class Rabbit extends Animal {
    constructor(name,age) {
        super(name);
        this.age =age;
	}

    run() {
        super.run();//animal run
        console.log('rabbit run');//rabbit run
    }
}

let rabbit = new Rabbit("White Rabbit",21);

console.log(rabbit);//Rabbit { name: 'White Rabbit', age: 21 }

我们不仅可以重写方法,还可以重写类字段。 不过,当我们在父类构造器中访问一个被重写的字段时,有一个诡异的行为,这与绝大多数其他编程语言都很不一样。

当父类构造器在派生的类中被调用时,它会使用被重写的方法。但是父类构造器总是会使用它自己字段的值,而不是被重写的那一个。


没看完。4/8/26 快要byteintern面试了,复习一波

静态属性和静态方法

它们以 static 关键字开头,静态方法可以在类上调用,而不是在单个对象上。

class User {
  static staticMethod() {
    alert(this === User);
  }
}

User.staticMethod(); // true

他们本质是一样的

class User { }

User.staticMethod = function() {
  alert(this === User);
};

User.staticMethod(); // true

它不是对象的方法,而是整个 class 的方法。 静态的属性是一样的。静态属性和方法是可被继承的。

受保护的、私有的属性和方法

受保护的属性通常以下划线 _ 作为前缀。

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) {
      value = 0;
    }
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }

}

// 创建咖啡机
let coffeeMachine = new CoffeeMachine(100);

// 加水
coffeeMachine.waterAmount = -10; // _waterAmount 将变为 0,而不是 -10

如果属性不想被修改,不设置setter即可

推荐使用者种:

class CoffeeMachine {
  _waterAmount = 0;

  setWaterAmount(value) {
    if (value < 0) value = 0;
    this._waterAmount = value;
  }

  getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

受保护的字段是可以继承的; 如果我们继承 class MegaMachine extends CoffeeMachine,那么什么都无法阻止我们从新的类中的方法访问 this._waterAmount 或 this._power。

私有的属性

这是最近加入的新特性。 私有属性和方法应该以 # 开头。它们只在类的内部可被访问。 在语言级别,# 是该字段为私有的特殊标志。我们无法从外部或从继承的类中访问它。

私有字段与公共字段不会发生冲突。我们可以同时拥有私有的 #waterAmount 和公共的 waterAmount 字段。

如果我们继承自 CoffeeMachine,那么我们将无法直接访问 #waterAmount。我们需要依靠 waterAmount getter/setter

扩展内建类(使用内建类做父类)

内建类却是一个例外。它们相互间不继承静态方法。

类检查
用于返回值
typeof原始数据类型
{}.toString原始数据类型,内建对象,包含 Symbol.toStringTag 属性的对象
instanceof对象
正如我们所看到的,从技术上讲,{}.toString 是一种“更高级的” typeof。

当我们使用类的层次结构(hierarchy),并想要对该类进行检查,同时还要考虑继承时,这种场景下 instanceof 操作符确实很出色。

Mixin模式

没看,

Mixin(混入)模式是一种在 JavaScript 中实现代码复用和模拟多继承的流行技术。它的核心思想是:将一个或多个对象(称为 Mixin)的方法或属性,“复制”到另一个类或对象的原型上,从而为目标对象增加新的功能,而无需使用传统的继承。

简单来说,Mixin 模式是一种“组合优于继承”的设计思想,它允许你像搭积木一样,将不同的功能模块组合到一个类中。

🤔 为什么需要 Mixin 模式?

JavaScript 的类(class)只支持单继承,即一个类只能 extends 一个父类。但在实际开发中,我们常常需要一个类具备多种独立的能力。例如,一个 User 类可能同时需要“可序列化”和“可事件化”的能力。

如果只用继承,我们可能需要创建复杂的继承链,这会导致代码耦合度高、难以维护。Mixin 模式提供了一种更灵活的横向扩展方式,让一个类轻松拥有多种能力。

🛠️ 如何实现 Mixin 模式?

在 JavaScript 中,主要有两种常见的实现方式:

使用 Object.assign() 进行属性复制

这是最直接、最常见的方法。通过 Object.assign() 将 Mixin 对象的属性浅拷贝到目标类的原型(prototype)上。

// 定义一个可飞行的 Mixin
const Flyable = {
  fly() {
    console.log('I can fly!');
  }
};

// 定义一个可游泳的 Mixin
const Swimmable = {
  swim() {
    console.log('I can swim!');
  }
};

// 定义一个基类
class Animal {}

// 将 Mixin 的功能混入到 Animal 类的原型中
Object.assign(Animal.prototype, Flyable, Swimmable);

// 创建实例并测试
const duck = new Animal();
duck.fly();  // 输出: I can fly!
duck.swim(); // 输出: I can swim!

优点:简单直观,易于理解和实现。缺点:

  • 方法冲突:如果多个 Mixin 包含同名方法,后混入的会覆盖先混入的,可能引发意外行为。
  • 浅拷贝:Object.assign() 是浅拷贝,如果 Mixin 的属性是对象,拷贝的只是引用,修改原始 Mixin 会影响目标对象。
  • 不支持 super:无法通过 super 关键字调用原型链上的同名方法。
使用高阶函数(类工厂)

这是一种更现代、更强大的方式。通过定义一个函数,该函数接收一个基类作为参数,并返回一个扩展后的新类。

// 定义一个返回新类的高阶函数 Mixin
const withFlying = (BaseClass) => {
  return class extends BaseClass {
    fly() {
      // 如果父类也有 fly 方法,可以通过 super 调用
      if (super.fly) super.fly();
      console.log('Flying with mixin!');
    }
  };
};

const withSwimming = (BaseClass) => {
  return class extends BaseClass {
    swim() {
      console.log('Swimming with mixin!');
    }
  };
};

// 定义一个基类
class Bird {}

// 通过嵌套调用 Mixin 函数来组合功能
class Duck extends withSwimming(withFlying(Bird)) {}

// 创建实例并测试
const donaldDuck = new Duck();
donaldDuck.fly();  // 输出: Flying with mixin!
donaldDuck.swim(); // 输出: Swimming with mixin!

优点:

  • 支持 super:可以正确地调用原型链上的方法,解决了方法冲突问题。
  • 组合性强:可以清晰地链式组合多个 Mixin。
  • 不污染原型:不会直接修改原始基类的原型。

缺点:写法相对复杂一些。

✨ Mixin 模式的优缺点
优点
  • 代码复用:将通用功能(如日志、事件处理、缓存等)抽离成独立的 Mixin,可以在多个类之间轻松共享,避免代码重复。
  • 灵活性高:可以按需组合功能,构建出行为各异的类,比僵化的继承层次结构更灵活。
  • 模拟多继承:在 JavaScript 单继承的限制下,提供了一种实现多继承效果的优雅方案。
缺点
  • 方法名冲突:当混入的多个对象包含同名方法时,需要手动处理冲突,否则会导致方法被覆盖。
  • 来源不明确:过度使用 Mixin 可能导致一个类的方法来自多个地方,增加了代码的追踪和维护难度。
  • 原型污染风险:直接修改原型(如使用 Object.assign)可能会带来意想不到的副作用。
🌐 实际应用:Vue.js 中的 Mixin

Vue.js 框架就广泛使用了 Mixin 的思想。你可以将组件中可复用的选项(如 data, methods, created 钩子等)定义在一个 Mixin 对象中,然后在多个组件中通过 mixins 选项引入。

// 定义一个 Mixin 对象
const myMixin = {
  data() {
    return {
      message: 'Hello from Mixin!'
    }
  },
  created() {
    console.log('Mixin created hook called.');
  }
};

// 在 Vue 组件中使用
const MyComponent = {
  mixins: [myMixin],
  data() {
    return {
      // 组件自己的数据
    }
  }
  // 当组件被创建时,会先执行 Mixin 中的 created 钩子
};

当 Mixin 和组件本身存在同名选项时,Vue 有一套明确的合并策略(例如,对于 data,组件的数据会优先于 Mixin 的数据),这解决了大部分冲突问题。

现代JS教程与阮一峰ES6 · 4原型与类/原型

直接看原型链吧

在编程中,我们经常会想获取并扩展一些东西。

例如,我们有一个 user 对象及其属性和方法,并希望将 admin 和 guest 作为基于 user 稍加修改的变体。我们想重用 user 中的内容,而不是复制/重新实现它的方法,而只是在其之上构建一个新的对象。

原型继承(Prototypal inheritance) 这个语言特性能够帮助我们实现这一需求。

在 JavaScript 中,对象有一个特殊的隐藏属性 Prototype(原型)(如规范中所命名的),它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型”。 ![Pasted image 20250626161702](../../../../_assets/b631a74-Pasted image 20250626161702.png) 当我们从 object 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。在编程中,这被称为“原型继承”。

属性 Prototype 是内部的而且是隐藏的,但是这儿有很多设置它的方式。

使用特殊的名字 __proto__:

let animal = {
    eats: true,
}

let rabbit = {
    jumps: true,
}

rabbit.__proto__ = animal;
console.log(rabbit.eats);//true
rabbit.eat();//eating!!!

如果 animal 有许多有用的属性和方法,那么它们将自动地变为在 rabbit 中可用。这种属性被称为“继承”。

原型链可以很长,但是有两个限制:

  • 不能形成闭环
  • ——proto——的值==只能是对象或者null==,其他的值将会自动的被忽略

只能有一个 Prototype。一个对象不能从其他两个对象获得继承。

__proto__ 是 Prototype 的因历史原因而留下来的 getter/setter 初学者常犯一个普遍的错误,就是不知道 __proto__ 和 Prototype 的区别。 请注意,__proto__ 与内部的 Prototype 不一样。__proto__ 是 Prototype 的 getter/setter。

__proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 ==Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__== 去 get/set 原型。

在写入和删除的时候,使用的是直接的对象,而不是原型。读取时是原型:

let animal = {
    eats: true,
    walk() {
        console.log('animal walk!!!');
    }
};

let rabbit = {
    __proto__: animal
};

rabbit.walk();//animal walk!!!

rabbit.walk = function () {
    console.log("Rabbit! Bounce-bounce!");
};

rabbit.walk(); // Rabbit! Bounce-bounce!

animal.walk();//animal walk!!!

![Pasted image 20250626164521](../../../../_assets/fa37dee-Pasted image 20250626164521.png)

访问器(accessor)属性是一个例外,因为赋值(assignment)操作是由 setter 函数处理的。因此,写入此类属性实际上与调用函数相同。

let user = {
    name: "John",
    surname: "Smith",

    set fullName(value) {
        [this.name, this.surname] = value.split(" ");
    },

    get fullName() {
        return `${this.name} ${this.surname}`;
    }
};

let admin = {
    __proto__: user,
    isAdmin: true
};

console.log(admin.fullName); // John Smith (*)

admin.name = 'Dano';

console.log(admin.fullName);//Dano Smith
console.log(user.fullName);//John Smith
console.log(user.name);//John

admin.fullName = "Alice Cooper"; //(**)

console.log(admin.fullName); // Alice Cooper,admin 的内容被修改了
console.log(user.fullName);  // John Smith,user 的内容被保护了

在 (*) 行中,属性 admin.fullName 在原型 user 中有一个 getter,因此它会被调用。在 (**) 行中,属性在原型中有一个 setter,因此它会被调用。

在上面的例子中可能会出现一个有趣的问题:在 set fullName(value) 中 this 的值是什么?属性 this.name 和 this.surname 被写在哪里:在 user 还是 admin?

答案很简单:this 根本不受原型的影响。 无论在哪里找到方法:在一个对象还是在原型中。在一个方法调用中,this 始终是点符号 . 前面的对象。 因此,setter 调用 admin.fullName= 使用 admin 作为 this,而不是 user。

也就是说,普通的属性写入是在对象上进行的。对于访问器属性(虚假的属性),他是调用的原型的方法来实现的,方法中的this却是对象的而不是原型的,那么设置的也就是对象的属性。

所以,方法是共享的,但对象状态不是。

Object.keys(obj)只会返回自己的属性,而不会返回原型的属性 而for...in会返回原型和对象的属性。使用内建方法obj.hasOwnProperty(key)如果属性是自己的就返回true,是原型的就返回false。

let user = {
    name: "John",
    surname: "Smith",

    set fullName(value) {
        [this.name, this.surname] = value.split(" ");
    },

    get fullName() {
        return `${this.name} ${this.surname}`;
    }
};

let admin = {
    __proto__: user,
    isAdmin: true
};

console.log(Object.keys(admin));//[ 'isAdmin' ]
for (let prop in admin) console.log(prop);
/**
isAdmin
name
surname
fullName
 */
for (let prop in admin) {
    if (admin.hasOwnProperty(prop)) {
        console.log('对象的属性:' + prop);

    } else {
        console.log('原型的属性:' + prop);

    }
};
/**
 * 对象的属性:isAdmin
原型的属性:name
原型的属性:surname
原型的属性:fullName
 */

这里我们有以下继承链:rabbit 从 animal 中继承,animal 从 Object.prototype 中继承(因为 animal 是对象字面量 {...},所以这是默认的继承),然后再向上是 null ![Pasted image 20250626172555](../../../../_assets/6a411e1-Pasted image 20250626172555.png) 这有一件很有趣的事儿。方法 rabbit.hasOwnProperty 来自哪儿?我们并没有定义它。从上图中的原型链我们可以看到,该方法是 Object.prototype.hasOwnProperty 提供的。换句话说,它是继承的。

……如果 for..in 循环会列出继承的属性,那为什么 hasOwnProperty 没有像 eats 和 jumps 那样出现在 for..in 循环中?

答案很简单:它是不可枚举的。就像 Object.prototype 的其他属性,hasOwnProperty 有 enumerable:false 标志。并且 for..in 只会列出可枚举的属性。这就是为什么它和其余的 Object.prototype 属性都未被列出。

在现代引擎中,从性能的角度来看,我们是从对象还是从原型链获取属性都是没区别的。它们(引擎)会记住在哪里找到的该属性,并在下一次请求中重用它。

构造函数的Prototype

这里的 F.prototype 指的是 F 的一个名为 "prototype" 的常规属性。这听起来与“原型”这个术语很类似,但这里我们实际上指的是具有该名字的常规属性。

==设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 Prototype 赋值为 animal”。==

let animal = {
    eats: true
};

function Rabbit(name) {
    this.name = name;
}

Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal

console.log(rabbit.eats);// true

function Rabbit2(name) {
    this.name = name;
    this.__proto__ = animal;
}

let rabbit2 = new Rabbit2('Dano');
console.log(rabbit2.eats);//true

也就是说,F.prototype=obj就是在F(){this.proto=obj}的效果是一样的。 ![Pasted image 20250626180234](../../../../_assets/7bb3c99-Pasted image 20250626180234.png) ==F.prototype 属性仅在 new F 被调用时使用,它为新对象的 Prototype 赋值。==

如果在创建之后,F.prototype 属性有了变化(F.prototype = &lt;another object&gt;),那么通过 new F 创建的新对象也将随之拥有新的对象作为 Prototype,但已经存在的对象将保持旧有的值。就是不会动态的改变,这符合常识。

每个函数都有 "prototype" 属性,即使我们没有提供它。 默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。 ![Pasted image 20250626180422](../../../../_assets/0a24e73-Pasted image 20250626180422.png) ……JavaScript 自身并不能确保正确的 "constructor" 函数值。

是的,它存在于函数的默认 "prototype" 中,但仅此而已。之后会发生什么 —— 完全取决于我们。 特别是,如果我们将整个默认 prototype 替换掉,那么其中就不会有 "constructor" 了。

就是说,每个函数都会有一个默认的prototype属性,这个属性的值是个对象(object),这个对象里面有一个constructor属性,值为构造函数F.prototype = {constructor: F}

通过这一点我们可以使用现在已经有的对象来构造一个对象:

function Rabbit(name) {
  this.name = name;
  alert(name);
}

let rabbit = new Rabbit("White Rabbit");
let rabbit2 = new rabbit.constructor("Black Rabbit");

![Pasted image 20250626182536](../../../../_assets/3bcb638-Pasted image 20250626182536.png)

一切都很简单,只需要记住几条重点就可以清晰地掌握了:

  • F.prototype 属性(不要把它与 Prototype 弄混了)在 new F 被调用时为新对象的 Prototype 赋值。
  • F.prototype 的值要么是一个对象,要么就是 null:其他值都不起作用。
  • "prototype" 属性仅当设置在一个构造函数上,并通过 new 调用时,才具有这种特殊的影响。

在常规对象上,prototype 没什么特别的:

let user = {
  name: "John",
  prototype: "Bla-bla" // 这里只是普通的属性
};
function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

alert( rabbit.eats ); // true
function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

Rabbit.prototype = {};

alert( rabbit.eats ); // ?

答案是true,应为一但rabbit构造出来它的prototype的引用所指向的对象就不会变了,而对象本身还是可以改变的这和const声明一个对象很像:

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

Rabbit.prototype.eats = false;

alert( rabbit.eats ); // ?

在这里,直接修改的对象里面的属性的值,而不是修改整个对象,就会成功。

原生的原型
let obj = {};
alert( obj ); //[object Object]

简短的表达式 obj = {} 和 obj = new Object() 是一个意思,其中 Object 就是一个内建的对象构造函数,其自身的 prototype 指向一个带有 toString 和其他方法的一个巨大的对象。 ![Pasted image 20250626215656](../../../../_assets/603d460-Pasted image 20250626215656.png) 当 new Object() 被调用(或一个字面量对象 {...} 被创建),按照前面章节中我们学习过的规则,这个对象的 Prototype 属性被设置为 Object.prototype

就是说,Object()这个函数的prototype属性指向一个巨大的对象,这和之前的:

let human = {
    age: 21,
}

function Users(name) {
    this.name = name;
    console.log(`obj ${name} created!!!`);

}

Users.prototype = human;

let users = new Users('dano');
console.log(users.__proto__);

的形式很相似。

也就是说,这个Object构造函数的prototype被那个巨大的对象赋值

let obj = {};

alert(obj.__proto__ === Object.prototype); // true
alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true

其他内建对象,像 Array、Date、Function 及其他,都在 prototype 上挂载了方法。

例如,当我们创建一个数组 [1, 2, 3],在内部会默认使用 new Array() 构造器。因此 Array.prototype 变成了这个数组的 prototype,并为这个数组提供数组的操作方法。这样内存的存储效率是很高的。

所有的内建原型顶端都是 Object.prototype。这就是为什么有人说“一切都从对象继承而来”。 ![Pasted image 20250626220815](../../../../_assets/c6f3555-Pasted image 20250626220815.png) 一些方法在原型上可能会发生重叠,例如,Array.prototype 有自己的 toString 方法来列举出来数组的所有元素并用逗号分隔每一个元素。正如我们之前看到的那样,Object.prototype 也有 toString 方法,但是 Array.prototype 在原型链上更近,所以数组对象原型上的方法会被使用。 ![Pasted image 20250626220850](../../../../_assets/30f7f7d-Pasted image 20250626220850.png)

基本数据类型,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 String、Number 和 Boolean 被创建。它们提供给我们操作字符串、数字和布尔值的方法然后消失。

这些对象对我们来说是无形地创建出来的。大多数引擎都会对其进行优化,但是规范中描述的就是通过这种方式。这些对象的方法也驻留在它们的 prototype 中,可以通过 String.prototype、Number.prototype 和 Boolean.prototype 进行获取。

特殊值 null 和 undefined 比较特殊。它们没有对象包装器,所以它们没有方法和属性。并且它们也没有相应的原型。

原生的原型是可以被修改的。例如,我们向 String.prototype 中添加一个方法,这个方法将对所有的字符串都是可用的。在开发的过程中,我们可能会想要一些新的内建方法,并且想把它们添加到原生原型中。但这通常是一个很不好的想法。

在现代编程中,只有一种情况下允许修改原生原型。那就是 polyfilling(垫片)。

Polyfilling 是一个术语,表示某个方法在 JavaScript 规范中已存在,但是特定的 JavaScript 引擎尚不支持该方法,那么我们可以通过手动实现它,并用以填充内建原型。

从原型中借用函数:

let obj = {
    0: "Hello",
    1: "world!",
    length: 2,
};

obj.join = Array.prototype.join;

console.log(obj.join(',')); // Hello,world!

上面这段代码有效,是因为内建的方法 join 的内部算法只关心正确的索引和 length 属性。它不会检查这个对象是否是真正的数组。许多内建方法就是这样。

另一种方式是通过将 obj.__proto__ 设置为 Array.prototype,这样 Array 中的所有方法都自动地可以在 obj 中使用了。

但是如果 obj 已经从另一个对象进行了继承,那么这种方法就不可行了(译注:因为这样会覆盖掉已有的继承。此处 obj 其实已经从 Object 进行了继承,但是 Array 也继承自 Object,所以此处的方法借用不会影响 obj 对原有继承的继承,因为 obj 通过原型链依旧继承了 Object)。请记住,我们一次只能继承一个对象。

原型方法和没有__proto__的对象

使用 obj.__proto__ 设置或读取原型被认为已经过时且不推荐使用(deprecated)了(已经被移至 JavaScript 规范的附录 B,意味着仅适用于浏览器)。

现代的获取/设置原型的方法有:

  • Object.getPrototypeOf(obj) —— 返回对象 obj 的 Prototype。
  • Object.setPrototypeOf(obj, proto) —— 将对象 obj 的 Prototype 设置为 proto。

__proto__ 不被反对的唯一的用法是在创建新对象时,将其用作属性:{ __proto__: ... }

Object.create(proto, [descriptors]) —— 利用给定的 proto 作为 Prototype 和可选的属性描述来创建一个空对象。

let animal = {
    eats: true,

};

let rabbit = Object.create(animal);

console.log(rabbit.eats); //true
console.log(Object.getPrototypeOf(rabbit) === animal);//true

Object.setPrototypeOf(rabbit, {});
console.log(rabbit.eats);//undefined

Object.create 方法更强大,因为它有一个可选的第二参数:属性描述器 描述器的格式与 属性标志和属性描述符 一章中所讲的一样:

let animal = {
    eats: true,

};

let rabbit = Object.create(animal, {
    likes: {
        value:true,
    }
});

console.log(rabbit.likes);//true
console.log(Object.getOwnPropertyDescriptors(rabbit));
/**
 * {
  likes: {
    value: true,
    writable: false,
    enumerable: false,
    configurable: false
  }
}
 */
console.log(rabbit.eats);//true

我们可以使用 Object.create 来实现比复制 for..in 循环中的属性更强大的对象克隆方式:

let clone = Object.create(
  Object.getPrototypeOf(obj),//原型
  Object.getOwnPropertyDescriptors(obj)//属性描述
);

是浅拷贝。

从技术上来讲,我们可以在任何时候 get/set Prototype。但是通常我们只在创建对象的时候设置它一次,自那之后不再修改:rabbit 继承自 animal,之后不再更改。

并且,JavaScript 引擎对此进行了高度优化。用 Object.setPrototypeOf 或 obj.__proto__= “即时”更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化。因此,除非你知道自己在做什么,或者 JavaScript 的执行速度对你来说完全不重要,否则请避免使用它。

let obj = {};

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // [object Object],并不是 "some value"!

这里如果用户输入 __proto__,那么在第四行的赋值会被忽略!

对于非开发者来说,这肯定很令人惊讶,但对我们来说却是可以理解的。__proto__ 属性很特殊:它必须是一个对象或者 null。字符串不能成为原型。这就是为什么将字符串赋值给 __proto__ 会被忽略。

通常开发者完全不会考虑到这一点。这让此类 bug 很难被发现,甚至变成漏洞,尤其是在 JavaScript 被用在服务端的时候。

现在,我们想要将一个对象用作关联数组,并且摆脱此类问题,我们可以使用一些小技巧:

let obj = Object.create(null);
// 或者:obj = { __proto__: null }

let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";

alert(obj[key]); // "some value"

Object.create(null) 创建了一个空对象,这个对象没有原型(Prototype 是 null) ![Pasted image 20250626224751](../../../../_assets/2076839-Pasted image 20250626224751.png) 因此,它没有继承 __proto__ 的 getter/setter 方法。现在,它被作为正常的数据属性进行处理,因此上面的这个示例能够正常工作。

我们可以把这样的对象称为 “very plain” 或 “pure dictionary” 对象,因为它们甚至比通常的普通对象(plain object){...} 还要简单。

大多数与对象相关的方法都是 Object.something(...),例如 Object.keys(obj) —— 它们不在 prototype 中,因此在 “very plain” 对象中它们还是可以继续使用:

let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";

alert(Object.keys(chineseDictionary)); // hello,bye
function Rabbit(name) {
    this.name = name;
}
Rabbit.prototype.sayHi = function () {
    console.log(this.name);
};

let rabbit = new Rabbit("Rabbit");

rabbit.sayHi();//Rabbit
Rabbit.prototype.sayHi();//undefined
Object.getPrototypeOf(rabbit).sayHi();//undefined
rabbit.__proto__.sayHi();//undefined

除了第一个的this是rabbit,剩下的都是Rabbit.prototype,所以都是undefined

现代JS教程与阮一峰ES6 · 4原型与类/原型链

原型链

原型链的全貌图 ![Pasted image 20260412184219](../../../../_assets/a2c32f4-Pasted image 20260412184219.png)

原型链的理解很简单,背这个图没有意义:

  • 一个对象有__proto__属性,隐式原型
  • 一个函数有prototype属性,原型属性
  • 一个对象由new调用其构造函数而来
  • 构造函数也是对象

画图的技巧: 一个对象的隐式原型和将他new出来的函数的原型属性是一样的

这里有两个特例:

  • Object.prototype.__proto__是null
  • Function.prototype===Function.__proto__

其余的按这个规律就可以全部画出

原型图中的关系均是正确的:

class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
}

const p1 = new Person('dano', 21)

console.log(p1.__proto__)
// {}
console.log(Person.prototype)
// {}
console.log(p1.__proto__ === Person.prototype)
// true

console.log(Object.prototype.__proto__)
// null

console.log(Object.__proto__ === Function.prototype)
// true

console.log(Function.prototype)
// [Function (anonymous)] Object

console.log(Function.prototype===Function.__proto__)
// true

console.log(Function.prototype.__proto__.__proto__)
// null

console.log(Function.prototype.__proto__===Person.prototype.__proto__)
// true

廖雪峰教程 · 6对象/6.0 简述

JavaScript的所有数据都可以看成对象,那是不是我们已经在使用面向对象编程了呢?

当然不是。如果我们只使用Number、Array、string以及基本的{...}定义的对象,还无法发挥出面向对象编程的威力。

JavaScript的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样。如果你熟悉Java或C#,很好,你一定明白面向对象的两个基本概念:

  1. 类:类是对象的类型模板,例如,定义Student类来表示学生,类本身是一种类型,Student表示学生类型,但不表示任何具体的某个学生;
  2. 实例:实例是根据类创建的对象,例如,根据Student类可以创建出xiaoming、xiaohong、xiaojun等多个实例,每个实例表示一个具体的学生,他们全都属于Student类型。

所以,类和实例是大多数面向对象编程语言的基本概念。

不过,在JavaScript中,这个概念需要改一改。JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。

原型是指当我们想要创建xiaoming这个具体的学生时,我们并没有一个Student类型可用。那怎么办?恰好有这么一个现成的对象:

let robot = {
    name: 'Robot',
    height: 1.6,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

我们看这个robot对象有名字,有身高,还会跑,有点像小明,干脆就根据它来“创建”小明得了!

于是我们把它改名为Student,然后创建出xiaoming:

let Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

let xiaoming = {
    name: '小明'
};

xiaoming.__proto__ = Student;

注意最后一行代码把xiaoming的原型指向了对象Student,看上去xiaoming仿佛是从Student继承下来的:

xiaoming.name; // '小明'
xiaoming.run(); // 小明 is running...

xiaoming有自己的name属性,但并没有定义run()方法。不过,由于小明是从Student继承而来,只要Student有run()方法,xiaoming也可以调用: xiaoming-prototype

JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。

如果你把xiaoming的原型指向其他对象:

let Bird = {
    fly: function () {
        console.log(this.name + ' is flying...');
    }
};

xiaoming.__proto__ = Bird;

现在xiaoming已经无法run()了,他已经变成了一只鸟:

xiaoming.fly(); // 小明 is flying...

在JavaScrip代码运行时期,你可以把xiaoming从Student变成Bird,或者变成任何对象。

请注意,上述代码仅用于演示目的。在编写JavaScript代码时,不要直接用obj.__proto__去改变一个对象的原型,并且,低版本的IE也无法使用__proto__。Object.create()方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有,因此,我们可以编写一个函数来创建xiaoming:

// 原型对象:
let Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

function createStudent(name) {
    // 基于Student原型创建一个新对象:
    let s = Object.create(Student);
    // 初始化新对象:
    s.name = name;
    return s;
}

let xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true

这是创建原型继承的一种方法,JavaScript还有其他方法来创建对象,我们在后面会一一讲到。

廖雪峰教程 · 6对象/6.1 创建对象

创建对象

JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象。

当我们用obj.xxx访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到Object.prototype对象,最后,如果还没有找到,就只能返回undefined。

例如,创建一个Array对象:

let arr = [1, 2, 3];

其原型链是:

       null
         ▲
         │
┌─────────────────┐
│Object.prototype │
└─────────────────┘
         ▲
         │
┌─────────────────┐
│ Array.prototype │
└─────────────────┘
         ▲
         │
┌─────────────────┐
│       arr       │
└─────────────────┘

Array.prototype定义了indexOf()、shift()等方法,因此你可以在所有的Array对象上直接调用这些方法。

当我们创建一个函数时:

function foo() {
    return 0;
}

函数也是一个对象,它的原型链是:

        null
          ▲
          │
┌───────────────────┐
│ Object.prototype  │
└───────────────────┘
          ▲
          │
┌───────────────────┐
│Function.prototype │
└───────────────────┘
          ▲
          │
┌───────────────────┐
│        foo        │
└───────────────────┘

由于Function.prototype定义了apply()等方法,因此,所有函数都可以调用apply()方法。

很容易想到,如果原型链很长,那么访问一个对象的属性就会因为花更多的时间查找而变得更慢,因此要注意不要把原型链搞得太长。

构造函数

除了直接用{ ... }创建一个对象外,JavaScript还可以用一种构造函数的方法来创建对象。它的用法是,先定义一个构造函数:

function Student(name) {
    this.name = name;
    this.hello = function () {
        alert('Hello, ' + this.name + '!');
    }
}

你会问,咦,这不是一个普通函数吗?

这确实是一个普通函数,但是在JavaScript中,可以用关键字new来调用这个函数,并返回一个对象:

let xiaoming = new Student('小明');
xiaoming.name; // '小明'
xiaoming.hello(); // Hello, 小明!

注意,如果不写new,这就是一个普通函数,它返回undefined。但是,如果写了new,它就变成了一个构造函数,它绑定的this指向新创建的对象,并默认返回this,也就是说,不需要在最后写return this;。

新创建的xiaoming的原型链是:

       null
         ▲
         │
┌─────────────────┐
│Object.prototype │
└─────────────────┘
         ▲
         │
┌─────────────────┐
│Student.prototype│
└─────────────────┘
         ▲
         │
┌─────────────────┐
│    xiaoming     │
└─────────────────┘

也就是说,xiaoming的原型指向函数Student的原型。如果你又创建了xiaohong、xiaojun,那么这些对象的原型与xiaoming是一样的:

              null
                ▲
                │
       ┌─────────────────┐
       │Object.prototype │
       └─────────────────┘
                ▲
                │
       ┌─────────────────┐
       │Student.prototype│
       └─────────────────┘
         ▲      ▲      ▲
         │      │      │
┌─────────┐┌─────────┐┌─────────┐
│xiaoming ││xiaohong ││ xiaojun │
└─────────┘└─────────┘└─────────┘

用new Student()创建的对象还从原型上获得了一个constructor属性,它指向函数Student本身:

xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true

Object.getPrototypeOf(xiaoming) === Student.prototype; // true

xiaoming instanceof Student; // true

看晕了吧?用一张图来表示这些乱七八糟的关系就是: prototypes

红色箭头是原型链。注意,Student.prototype指向的对象就是xiaoming、xiaohong的原型对象,这个原型对象自己还有个属性constructor,指向Student函数本身。

另外,函数Student恰好有个属性prototype指向xiaoming、xiaohong的原型对象,但是xiaoming、xiaohong这些对象可没有prototype这个属性,不过可以用__proto__这个非标准用法来查看。

现在我们就认为xiaoming、xiaohong这些对象“继承”自Student。

不过还有一个小问题,注意观察:

xiaoming.name; // '小明'
xiaohong.name; // '小红'
xiaoming.hello; // function: Student.hello()
xiaohong.hello; // function: Student.hello()
xiaoming.hello === xiaohong.hello; // false

xiaoming和xiaohong各自的name不同,这是对的,否则我们无法区分谁是谁了。

xiaoming和xiaohong各自的hello是一个函数,但它们是两个不同的函数,虽然函数名称和代码都是相同的!

如果我们通过new Student()创建了很多对象,这些对象的hello函数实际上只需要共享同一个函数就可以了,这样可以节省很多内存。

要让创建的对象共享一个hello函数,根据对象的属性查找原则,我们只要把hello函数移动到xiaoming、xiaohong这些对象共同的原型上就可以了,也就是Student.prototype: protos2

修改代码如下:

function Student(name) {
    this.name = name;
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
};

用new创建基于原型的JavaScript的对象就是这么简单!

忘记写new怎么办

如果一个函数被定义为用于创建对象的构造函数,但是调用时忘记了写new怎么办?

在strict模式下,this.name = name将报错,因为this绑定为undefined,在非strict模式下,this.name = name不报错,因为this绑定为window,于是无意间创建了全局变量name,并且返回undefined,这个结果更糟糕。

所以,调用构造函数千万不要忘记写new。为了区分普通函数和构造函数,按照约定,构造函数首字母应当大写,而普通函数首字母应当小写,这样,一些语法检查工具如jslint将可以帮你检测到漏写的new。

最后,我们还可以编写一个createStudent()函数,在内部封装所有的new操作。一个常用的编程模式像这样:

function Student(props) {
    this.name = props.name || '匿名'; // 默认值为'匿名'
    this.grade = props.grade || 1; // 默认值为1
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
};

function createStudent(props) {
    return new Student(props || {})
}

这个createStudent()函数有几个巨大的优点:一是不需要new来调用,二是参数非常灵活,可以不传,也可以这么传:

let xiaoming = createStudent({
    name: '小明'
});

xiaoming.grade; // 1

如果创建的对象有很多属性,我们只需要传递需要的某些属性,剩下的属性可以用默认值。由于参数是一个Object,我们无需记忆参数的顺序。如果恰好从JSON拿到了一个对象,就可以直接创建出xiaoming。

廖雪峰教程 · 6对象/6.2 原型继承

原型继承

在传统的基于Class的语言如Java、C++中,继承的本质是扩展一个已有的Class,并生成新的Subclass。

由于这类语言严格区分类和实例,继承实际上是类型的扩展。但是,JavaScript由于采用原型继承,我们无法直接扩展一个Class,因为根本不存在Class这种类型。

但是办法还是有的。我们先回顾Student构造函数:

function Student(props) {
    this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

以及Student的原型链: js-proto

现在,我们要基于Student扩展出PrimaryStudent,可以先定义出PrimaryStudent:

function PrimaryStudent(props) {
    // 调用Student构造函数,绑定this变量:
    Student.call(this, props);
    this.grade = props.grade || 1;
}

但是,调用了Student构造函数不等于继承了Student,PrimaryStudent创建的对象的原型是:

new PrimaryStudent() --> PrimaryStudent.prototype --> Object.prototype --> null

必须想办法把原型链修改为:

new PrimaryStudent() --> PrimaryStudent.prototype --> Student.prototype --> Object.prototype --> null

这样,原型链对了,继承关系就对了。新的基于PrimaryStudent创建的对象不但能调用PrimaryStudent.prototype定义的方法,也可以调用Student.prototype定义的方法。

如果你想用最简单粗暴的方法这么干:

PrimaryStudent.prototype = Student.prototype;

是不行的!如果这样的话,PrimaryStudent和Student共享一个原型对象,那还要定义PrimaryStudent干啥?

我们必须借助一个中间对象来实现正确的原型链,这个中间对象的原型要指向Student.prototype。为了实现这一点,参考道爷(就是发明JSON的那个道格拉斯)的代码,中间对象可以用一个空函数F来实现:

// PrimaryStudent构造函数:
function PrimaryStudent(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 空函数F:
function F() {
}

// 把F的原型指向Student.prototype:
F.prototype = Student.prototype;

// 把PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype:
PrimaryStudent.prototype = new F();

// 把PrimaryStudent原型的构造函数修复为PrimaryStudent:
PrimaryStudent.prototype.constructor = PrimaryStudent;

// 继续在PrimaryStudent原型(就是new F()对象)上定义方法:
PrimaryStudent.prototype.getGrade = function () {
    return this.grade;
};

// 创建xiaoming:
let xiaoming = new PrimaryStudent({
    name: '小明',
    grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2

// 验证原型:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true

// 验证继承关系:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true

用一张图来表示新的原型链: js-proto-inherit

注意,函数F仅用于桥接,我们仅创建了一个new F()实例,而且,没有改变原有的Student定义的原型链。

如果把继承这个动作用一个inherits()函数封装起来,还可以隐藏F的定义,并简化代码:

function inherits(Child, Parent) {
    let F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

这个inherits()函数可以复用:

function Student(props) {
    this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

function PrimaryStudent(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 实现原型继承链:
inherits(PrimaryStudent, Student);

// 绑定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function () {
    return this.grade;
};

廖雪峰教程 · 6对象/6.3 类的继承

类的继承

在上面的章节中我们看到了JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。

有没有更简单的写法?有!

新的关键字class从ES6开始正式被引入到JavaScript中。class的目的就是让定义类更简单。

我们先回顾用函数实现Student的方法:

function Student(name) {
    this.name = name;
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

如果用新的class关键字来编写Student,可以这样写:

class Student {
    constructor(name) {
        this.name = name;
    }

    hello() {
        alert('Hello, ' + this.name + '!');
    }
}

比较一下就可以发现,class的定义包含了构造函数constructor和定义在原型对象上的函数hello()(注意没有function关键字),这样就避免了Student.prototype.hello = function () {...}这样分散的代码。

最后,创建一个Student对象代码和前面章节完全一样:

let xiaoming = new Student('小明');
xiaoming.hello();
class继承

用class定义对象的另一个巨大的好处是继承更方便了。想一想我们从Student派生一个PrimaryStudent需要编写的代码量。现在,原型继承的中间对象,原型对象的构造函数等等都不需要考虑了,直接通过extends来实现:

class PrimaryStudent extends Student {
    constructor(name, grade) {
        super(name); // 记得用super调用父类的构造方法!
        this.grade = grade;
    }

    myGrade() {
        alert('I am at grade ' + this.grade);
    }
}

注意PrimaryStudent的定义也是class关键字实现的,而extends则表示原型链对象来自Student。子类的构造函数可能会与父类不太相同,例如,PrimaryStudent需要name和grade两个参数,并且需要通过super(name)来调用父类的构造函数,否则父类的name属性无法正常初始化。

PrimaryStudent已经自动获得了父类Student的hello方法,我们又在子类中定义了新的myGrade方法。

ES6引入的class和原有的JavaScript原型继承有什么区别呢?实际上它们没有任何区别,class的作用就是让JavaScript引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class的好处就是极大地简化了原型链代码。